package org.smartboot.compare.comparator;

import org.smartboot.compare.ComparatorContext;
import org.smartboot.compare.ComparatorRegister;
import org.smartboot.compare.FieldCache;
import org.smartboot.compare.NameType;
import org.smartboot.compare.Option;
import org.smartboot.compare.Path;
import org.smartboot.compare.difference.Difference;
import org.smartboot.compare.difference.DifferenceGroup;
import org.smartboot.compare.difference.NullOfOneObject;
import org.smartboot.compare.difference.SizeDifference;
import org.smartboot.compare.utils.ComparatorUtils;

import java.util.HashSet;
import java.util.Map;
import java.util.Set;

/**
 * @author qinluo
 * @version 1.0.0
 * @since 2019-05-27 22:53
 */
public class MapComparator extends AbstractComparator<Map<?, ?>> {

    @Override
    @SuppressWarnings({"rawtypes", "unchecked"})
    public Difference compare(Map<?, ?> expect, Map<?, ?> actual, ComparatorContext<Map<?, ?>> context) {
        if (ComparatorUtils.checkEmpty(expect, actual) && context.hasOption(Option.LOOSE_MODE)) {
            return Difference.SAME;
        }

        //有一个为空
        if (expect == null || actual == null) {
            return new NullOfOneObject(context.getPath(), expect, actual);
        }

        int expectSize = expect.size();
        int actualSize = actual.size();

        DifferenceGroup group = DifferenceGroup.of();

        if (expectSize != actualSize) {
            group.addDifference(new SizeDifference(context.getPath(), expectSize, actualSize, expect, actual));
        }

        if (group.hasDifferences() && context.hasOption(Option.IMMEDIATELY_INTERRUPT)) {
            return group;
        }

        Set<?> expectSet = expect.keySet();
        Set<?> actualKeySet = actual.keySet();
        Set<Object> allKeySet = new HashSet<>(expectSet);
        allKeySet.addAll(actualKeySet);
        for (Object objKey : allKeySet) {
            Object objValue = expect.get(objKey);
            Object compareValue = actual.get(objKey);

            if (ComparatorUtils.checkAllNull(objValue, compareValue)) {
                continue;
            }

            Class appliedType = applyNamedTypeCompare(objValue, compareValue, context);
            Path newPath = context.createKeyPath(objKey);
            if (appliedType != null && isIgnoreFields(context, objKey, appliedType, newPath)) {
                context.addSkippedField(newPath.getFullPath());
                continue;
            }

            ComparatorContext<Object> newContext = context.clone(objValue, compareValue);
            newContext.incr();
            newContext.setPath(newPath);

            boolean customized = false;
            if (appliedType != null) {
                NameType nameType = NameType.of(appliedType, String.valueOf(objKey));
                Comparator comparator = ComparatorRegister.findComparator(nameType);
                if (comparator != null) {
                    customized = true;
                    group.addDifference(comparator.compare(newContext));
                }
            }

            if (!customized) {
                group.addDifference(ComparatorRegister.dispatcherComparator().compare(newContext));
            }

            if (group.hasDifferences() && context.hasOption(Option.IMMEDIATELY_INTERRUPT)) {
                break;
            }
        }

        return group;
    }

    private boolean isIgnoreFields(ComparatorContext<?> context, Object key, Class<?> type, Path newPath) {
        FieldCache fc = new FieldCache(key.toString(), type);
        return context.getFilters().filtered(fc, context, newPath);
    }

    private Class<?> applyNamedTypeCompare(Object expect, Object actual, ComparatorContext<?> ctx) {
        Class<?> expectType = expect != null ? expect.getClass() : null;
        Class<?> actualType = actual != null ? actual.getClass() : null;

        if (expectType == null || actualType == null) {
            return expectType != null ? expectType : actualType;
        }

        if (expectType == actualType || expectType.isAssignableFrom(actualType)) {
            return expectType;
        }

        if (actualType.isAssignableFrom(expectType)) {
            return actualType;
        }

        if (ctx.hasOption(Option.USE_EXPECT_TYPE_IN_MAP)) {
            return expectType;
        }

        return null;
    }
}
